using System;
using System.Threading;
using System.Linq;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Excel;
using Microsoft.Office.Core;
using Action = System.Action;
using MsoDocProperties = Microsoft.Office.Core.MsoDocProperties;


namespace ExcelRefresh.Com
{
    public abstract class ComHelpers
    {
        /// <summary>
        /// Executes an Action within a code block that guarantees the release of an underlying COM object.
        /// </summary>
        public static void UsingCOM<T>(T reference, Action<T> doThis) where T : class
        {
            UsingCOM(reference, doThis, null);
        }

        /// <summary>
        /// Executes an Action within a code block that guarantees the release of an underlying COM object.
        /// </summary>
        public static void UsingCOM<T>(T reference, Action<T> doThis, Action<T> thenFinally) where T : class
        {
            if (reference == null) return;
            try
            {
                doThis(reference);
            }
            finally
            {
                if (thenFinally == null)
                    while (Marshal.ReleaseComObject(reference) > 0) { }
                else
                    try
                    {
                        thenFinally(reference);
                    }
                    finally
                    {
                        while (Marshal.ReleaseComObject(reference) > 0) { }
                    }

            }
        }


        public static void UsingAutomationApp<T>(
                      T app,
                      Func<T, int> fnGethWnd,
                      Action<T, Process> doThis) where T : class
        {
            using (new OleMessageFilter())
            {
                var process = GetProcessFromHWnd(fnGethWnd(app));
                try
                {
                    doThis(app, process);
                }
                finally
                {
                    while (Marshal.ReleaseComObject(app) > 0) { }
                    GC.Collect();
                    GC.WaitForPendingFinalizers();
                    GC.GetTotalMemory(true);

                    if (!process.HasExited)
                        process.Kill();
                }
            }
        }


        [DllImport("user32.dll")]
        static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);

        public static Process GetProcessFromHWnd(int hWnd)
        {
            int processId;
            GetWindowThreadProcessId(hWnd, out processId);
            return Process.GetProcessById(processId);
        }

        public static void UsingExcel(Action<Application> doThis)
        {
            UsingAutomationApp(
                new Application { Visible = false, DisplayAlerts = false },
                app => app.Hwnd,
                (app, process) =>
                {
                    app.MultiThreadedCalculation.Enabled = true;
                    app.IgnoreRemoteRequests = true;
#if DEBUG
                    //app.DisplayAlerts = true;
                    app.DisplayAlerts = false;
#else
                    app.DisplayAlerts = false;
#endif
                    try
                    {
                        doThis(app);
                    }
                    finally
                    {
                        app.IgnoreRemoteRequests = false;
                        app.Quit();
                    }
                });
        }

        private static int OpenPowerPivotFiles = 0;
        private static readonly object openLock = new object();

        public static void UsingWorkbook(string workbookFilePath, params Action<Workbook>[] doThisWork)
        {
        
            UsingExcel(excelApp =>
            {
                Workbook book;
                lock (openLock)
                {
                    Trace.TraceInformation("Opening {0}", workbookFilePath);
                    try
                    {
                        book = excelApp.Workbooks.Open(workbookFilePath,
                                                    XlUpdateLinks.xlUpdateLinksNever   /* [don't] Update links */                       ,
                                                    false                              /* Readonly OK? */                               ,
                                                    Type.Missing                       /* Format */                                     ,
                                                    Type.Missing                       /* Password */                                   ,
                                                    Type.Missing                       /* Write Res Password */                         ,
                                                    true                               /* Ignore Readonly Recommended */                ,
                                                    Type.Missing                       /* Origin (codepage)*/                           ,
                                                    Type.Missing                       /* Delimiter */                                  ,
                                                    Type.Missing                       /* Enable editable addins */                     ,
                                                    false                              /* Open in notify mode if locked by other user? */
                                                    );
                    }
                    catch (Exception ex)
                    {
                        Trace.TraceInformation("Error Opening {0} {1}", workbookFilePath, ex.Message);
                        throw ex;
                    }
                    Trace.TraceInformation("Opened {0}", workbookFilePath);
                }

                UsingCOM
                (
                    book,
                    workBook =>
                    {
                        var hasPowerPivot = workBook.Connections.OfType<WorkbookConnection>().Any(c => c.Name == "ThisWorkbookDataModel");
                        
                        if (hasPowerPivot)
                            if (Interlocked.Increment(ref OpenPowerPivotFiles) > 3)
                            {
                                Interlocked.Decrement(ref OpenPowerPivotFiles);
                                Trace.TraceInformation("Re-queueing {0}", workbookFilePath);
                                workBook.Close(false);
                                throw new TooManyPowerPivotFilesException();
                            }
                            else
                            {
                                // Take a 30 second nap to allow PowerPivot to restore itself
                                Thread.Sleep(TimeSpan.FromSeconds(30));
                            }
                        try
                        {
                            for (var i = 0; i < doThisWork.Length; i++) doThisWork[i](workBook);
                        }
                        finally
                        {
                            Trace.TraceInformation("Closing {0}", workbookFilePath);
                            workBook.Close(false);

                            if (hasPowerPivot)
                                Interlocked.Decrement(ref OpenPowerPivotFiles);
                        }
                    }
                );
                }
            );
        }
    }

    [Serializable]
    public class TooManyPowerPivotFilesException : Exception
    {
        public TooManyPowerPivotFilesException() { }
        public TooManyPowerPivotFilesException(string message) : base(message) { }
        public TooManyPowerPivotFilesException(string message, Exception inner) : base(message, inner) { }
        protected TooManyPowerPivotFilesException(
          System.Runtime.Serialization.SerializationInfo info,
          System.Runtime.Serialization.StreamingContext context)
            : base(info, context) { }
    }
}